fix: 7 reported findings - #2578
Merged
Merged
Conversation
|
Claude finished @ethicnology's task in 6m 33s —— View job Reviewing PR #2578
|
The Sell/Pay confirm flow built the payin from the live order object, and both the price-lock refresh and the periodic order poll replaced that object unconditionally with the latest server response. A tampered or compromised response could swap the deposit address mid-order and silently redirect the payment; the confirm screen never showed the address, so even a careful user had no way to notice. Refuse to adopt a refreshed or polled order whose deposit address differs from the one the order was created with, surface a localized error instead, and display the deposit address on both confirm screens so the destination is verifiable. The payin keeps targeting the creation-time address, mirroring the pinning SwapWatcher already does for swap claim/refund addresses. Audit reproducers cover the refresh path, the poll path and the confirm-time address actually handed to the PSBT build, for both Sell and Pay.
The unlock "lockout" was cosmetic: AttemptUnlockWithPinCodeUsecase always verified the PIN first and only computed a timeout afterwards for a countdown label, nothing gated re-submission on it, and the dial pad had no disabled state. A 4-digit PIN is 10,000 combinations with no enforced delay between attempts. Anchor the cooldown to a persisted wall-clock timestamp so it survives an app restart, and refuse to even compare the PIN while it runs — a brute-forcer gets neither a signal nor an attempt rate, whatever entry point drives the attempts. The remaining time is what the UI counts down (recomputing it from the attempt count restarted a full timeout on every launch), the submit path and the dial pad are inert for the duration, and a successful unlock clears the lockout. Tests cover the domain gate (no verification during a cooldown, an expired lockout lets attempts through), the timestamp persistence and the remaining-time computation.
A directly-connected Ledger or BitBox returns raw signed bytes that updateSignedBitcoinTx stored verbatim and onConfirmTransactionClicked broadcast unchecked, while the confirm screen kept showing the pre-signing address and amount. A compromised device or transport could redirect the payment or the change and the user had no way to notice — the QR/air-gapped signer path already reviews the decoded transaction instead. Decode the signed transaction and compare its output set (scriptPubkey + amount, in order) against the unsigned PSBT the app built: signing only adds witnesses, so any difference means tampering. A mismatch is refused with a confirmation error, never stored for broadcast. The usecase test runs the real bdk decoding in the host test VM (bdk_dart ships a native-assets hook) with tampered amount, tampered script and undecodable vectors; the cubit test pins the refuse/accept behavior.
downloadAttachment concatenated the server-supplied fileName verbatim into a File() path, so a crafted getMessageAttachment response (a compromised support-chat backend, a rogue agent, or a MITM) could write attacker bytes outside the temp directory — anywhere in the app's own sandbox, including over its local databases. Reduce the name to its last path segment (normalizing Windows separators too) and fall back to the opaque attachment id when nothing usable remains; the original name is kept for the share-sheet display label only. Tests cover the sanitizer and, end to end through the cubit with a fake path_provider, that a traversal payload writes only inside the temp directory.
ethicnology
force-pushed
the
security-audit
branch
from
August 10, 2026 18:48
80beab3 to
442d467
Compare
Opening Settings → View all seeds read every wallet's mnemonic from secure storage as soon as the screen built — one tap away from an unlocked app, behind only a warning dialog. The app already demands step-up re-authentication before changing the PIN; the screen that exposes full custody of every wallet now gets the same gate. The cubit enforces it, not just the UI: no code path pulls raw seed phrases out of secure storage before unlock() lands, and the screen shows the existing PinCodeUnlockScreen until then (it succeeds immediately when no PIN is set). Tests pin the gate: seeds are never read pre-unlock, unlock() fetches exactly once.
A Liquid payment to an unconfidential (unblinded) address puts the amount and asset id on-chain in the clear, and nothing in the send flow distinguished that from a confidential address — a merchant or a copy-paste could silently strip the payment of Liquid's privacy properties. Detect confidentiality structurally (blech32 HRPs lq1/tlq1, bech32 ex1/tex1, and the doubled length of a Base58Check address carrying a blinding key) and flag the destination on the confirm screen with a warning card. Computing it at the consumption point, rather than carrying a field on PaymentRequest, covers the plain-address and the liquidnetwork BIP21 paths uniformly. Tests cover the classifier, the SendState getter, and on-device parsing of a real lwk-derived unconfidential address.
The pinned recoverbull-client-dart (9fd986d) decrypts the key-server's encrypted_secret without verifying its trailing HMAC: a compromised key server can return a tampered ciphertext that decrypts silently. Demonstrated against the pinned client — fetchBackupKey returns a forged 32-byte backup key whose HMAC was flipped. Impact is bounded by the local vault decryption verifying its own HMAC downstream, but the server-side check itself is a no-op. The fix lives in the dependency (its telemetry branch, pulled in by the pin bump planned after PR #2560's client merges upstream), not in this repo. The regression test asserts the secure behavior and stays skipped until the pin moves past the fix; SECURITY.md documents the gap so it is not re-reported.
ethicnology
force-pushed
the
security-audit
branch
from
August 10, 2026 19:10
442d467 to
d494b56
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Responds to the external static-audit report of 7 findings against
8b64b17f1b86Every finding verified as real at the audited commit. Each was demonstrated with a failing-behavior reproducer run against
develop(scratch tests asserting the vulnerable behavior, run in a throwaway worktree), then fixed with a regression test asserting the secure behavior. One independently revertable commit per finding, plus one integration-fixture isolation commit required to keep the funded Payjoin test deterministic in the aggregated CI suite.Findings and fixes
| # | Finding (severity) | Verdict | Fix |
4.
fix(exchange_support_chat): sanitize attachment download filenames5.
feat(all_seed_view): require PIN re-authentication to reveal seeds— gate enforced before seed reads and composed through the publishedAppUnlockFacade; the dependency is recorded inFEATURES.md.6.
feat(send): warn when sending to an unconfidential Liquid address— detection computed at the consumption point rather than carried onPaymentRequest, so plain-address, BIP21, direct Liquid and cross-chain swap paths are covered uniformly.7.
test(recoverbull): guard the key-server fetch HMAC fix— pins the isolated recoverbull-client-dart fix commit92925c959f219fa4dfd0fe0f40392ddf868e2774; the formerly skipped test now runs and passes.8.
test(payjoin): isolate funded fixture environment— scopes funded-wallet setup to the skipped group, reapplies testnet immediately before the Payjoin flow, and restores both the previous Payjoin policy and mainnet afterward. This preventscoins_testfrom leaving the aggregated suite on mainnet and causing testnet transactions to fail asnon-finalon mainnet Electrum servers.Demonstration evidence (vulnerable behavior on
develop)F1: refreshed and polled orders carrying a different deposit address were adopted into payment state (scratch bloc tests, green on
develop).make unit-test: green, including the active RecoverBull HMAC regression test.make analyze: clean.dart fix --dry-run: "Nothing to fix!".make format-checkandmake bull-ui-check: green.make checksreachesfix-checkbut itstee /dev/stderrpipeline is incompatible with this agent shell (No such device or address); the same gates were run individually and passed.Integration:
integration_test/payment_request_test.dart(incl. the F2 on-device reproducer) passes on the Linux desktop device underxvfb-run. The previous CI run exposed a shared-environment leak in the funded Payjoin fixture; commit 8 fixes it, and the post-push CI rerun is pending. The fullintegration_test/all_test.dartsuite remains environment-blocked in this local headless VM (KeyringLockedfrom flutter_secure_storage).Not run here: on-device Android integration (
flutter testcannot attach to the Pixel from this VM — adb forwards bind on the host's loopback).Test plan
make unit-test: 1004 passed, 1 skipped (the F4 guard), 0 failures.make analyze: clean.dart fix --dry-run: "Nothing to fix!".make checks: green.integration_test/payment_request_test.dart(incl. the F2 on-device reproducer) passes on the Linux desktop device underxvfb-run. The fullintegration_test/all_test.dartsuite is environment-blocked in this headless VM (KeyringLockedfrom flutter_secure_storage — pre-existing, unrelated to these changes).flutter testcannot attach to the Pixel from this VM — adb forwards bind on the host's loopback).Out of scope / follow-ups
/app-unlockbuilds whenisPinCodeSetis a reasonable follow-up.security@bullbitcoin.combounces for external senders (Google Group not open to posting) — this report nearly never arrived. Consider opening the group or publishing an additional channel.